/*
 *  Copyright 2009 Jamie Gennis <jgennis@gmail.com>.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */

package com.google.code.gwt.remoteaction.batch.client;

import java.util.ArrayList;

import com.google.code.gwt.remoteaction.client.Action;
import com.google.code.gwt.remoteaction.client.ActionExecutor;
import com.google.code.gwt.remoteaction.client.ActionFilter;
import com.google.code.gwt.remoteaction.client.ActionProxy;
import com.google.code.gwt.remoteaction.client.ActionResult;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * An action filter that batches execution of actions so that a request can be
 * sent to the server containing multiple actions to execute.
 * 
 * @author Jamie Gennis <jgennis@gmail.com>
 */
public abstract class AbstractBatchingActionFilter implements ActionFilter {

  final ActionBatchExecutionServiceAsync actionBatchExecutor;

  final ActionProxy actionProxy;

  ArrayList<Action> queuedActions = new ArrayList<Action>();

  ArrayList<AsyncCallback<ActionResult>> queuedCallbacks =
      new ArrayList<AsyncCallback<ActionResult>>();

  boolean flushPending = false;

  /**
   * Construct a new {@code AbstractBatchingActionFilter} with a newly created
   * action batch execution service.
   */
  public AbstractBatchingActionFilter() {
    this((ActionBatchExecutionServiceAsync) GWT.create(ActionBatchExecutionService.class));
  }

  /**
   * Construct a new {@code AbstractBatchingActionFilter} instance with a given
   * action batch execution service.
   * 
   * @param actionBatchExecutor the action batch execution service async
   *          interface
   */
  public AbstractBatchingActionFilter(ActionBatchExecutionServiceAsync actionBatchExecutor) {
    if (actionBatchExecutor == null)
      throw new IllegalArgumentException("The 'actionBatchExecutor' argument may not be null");
    this.actionBatchExecutor = actionBatchExecutor;
    try {
      this.actionProxy = (ActionProxy) actionBatchExecutor;
    } catch (ClassCastException ex) {
      throw new IllegalArgumentException("The 'actionBatchExecutor' argument must implement the "
          + "ActionProxy interface", ex);
    }
  }

  /**
   * Set the action executor for the underlying action batch execution service.
   * 
   * @param actionExecutor the action executor
   */
  @Override
  public final void setActionExecutor(ActionExecutor actionExecutor) {
    actionProxy.setActionExecutor(actionExecutor);
  }

  /**
   * Get the action executor for the underlying action batch execution service.
   * 
   * @return the action executor
   */
  @Override
  public final ActionExecutor getActionExecutor() {
    return actionProxy.getActionExecutor();
  }

  /**
   * Enqueue an action/callback pair for execution.
   * 
   * If a deferred flush is not already pending, {@link #deferFlush()} is called
   * to enqueue a flush at some time determined by the child class. A deferred
   * flush is pending if {@link #deferFlush()} has been called, but
   * {@link #flush()} has not yet been called.
   * 
   * @param action the action to enqueue
   * @param callback the callback for when the action has completed
   */
  @Override
  public final void execute(Action action, AsyncCallback<ActionResult> callback) {
    if (action == null) {
      throw new IllegalArgumentException("The 'action' field may not be null");
    } else if (callback == null) {
      throw new IllegalArgumentException("The 'callback' field may not be null");
    }
    queuedActions.add(action);
    queuedCallbacks.add(callback);
    if (!flushPending) {
      flushPending = true;
      deferFlush();
    }
  }

  /**
   * Flush all the queued actions in a single action batch sent to the server.
   * 
   * After execution of the action batch completes, the individual action
   * results are passed to the corresponding callbacks. If a failure occurs
   * while executing a single action in the batch, the other actions are
   * unaffected. If a failure occurs while executing the action batch, that
   * failure (or rather the {@link Throwable} object indicating the failure) is
   * passed to each of the callbacks'
   * {@link AsyncCallback#onFailure(java.lang.Throwable)} method
   */
  protected final void flush() {
    flushPending = false;
    if (!queuedCallbacks.isEmpty()) {
      final ArrayList<Action> actions = queuedActions;
      final ArrayList<AsyncCallback<ActionResult>> callbacks = queuedCallbacks;
      queuedActions = new ArrayList<Action>();
      queuedCallbacks = new ArrayList<AsyncCallback<ActionResult>>();
      actionBatchExecutor.execute(actions, new AsyncCallback<ArrayList<ActionResult>>() {

        public void onFailure(final Throwable caught) {
          /*
           * XXX: Should this failure be broadcast to all the actions or just
           * thrown? Should it be wrapped in another exception?
           */
          for (final AsyncCallback<ActionResult> callback : callbacks) {
            /*
             * Use a deferred command so that if the callback throws an
             * unchecked exception it won't prevent the other callbacks from
             * being called.
             */
            addDeferredCommand(new Command() {

              @Override
              public void execute() {
                callback.onFailure(caught);
              }
            });
          }
        }

        public void onSuccess(ArrayList<ActionResult> results) {
          if (results.size() != callbacks.size()) {
            throw new RuntimeException("Got the wrong number of results while executing an "
                + "action batch");
          }
          for (int i = 0; i < results.size(); i++) {
            final ActionResult result = results.get(i);
            final AsyncCallback<ActionResult> callback = callbacks.get(i);
            /*
             * Use a deferred command so that if the callback throws an
             * unchecked exception it won't prevent the other callbacks from
             * being called.
             */
            addDeferredCommand(new Command() {

              @Override
              public void execute() {
                callback.onSuccess(result);
              }
            });
          }
        }
      });
    }
  }

  /**
   * Add a deferred command that will be executed once all events have been
   * handled.
   * 
   * This method exists so that it can be mocked for unit testing. It simply
   * calls {@link DeferredCommand#addCommand(Command)}.
   * 
   * @param command the command to execute
   */
  void addDeferredCommand(Command command) {
    DeferredCommand.addCommand(command);
  }

  /**
   * Defer a call to the {@link AbstractBatchingActionFilter#flush()} method
   * until some time determined by the child class.
   */
  protected abstract void deferFlush();
}
